import { merge, compose, omit, is } from "@wsvaio/utils";

type Context<C = {}, P = {}> = {
  q: Partial<P> & Record<any, any>;
  b: Partial<P> & Record<any, any>;
  p: Partial<P> & Record<any, any>;
  body: any;
  baseURL: string;

  befores: Middleware<C, P>[];
  afters: Middleware<C, P>[];
  errors: Middleware<C, P>[];
  finals: Middleware<C, P>[];

  successResult?: UniApp.RequestSuccessCallbackResult;
  failResult?: UniApp.GeneralCallbackResult;
  error?: Error;
} & UniApp.RequestOptions &
  C;

type Middleware<C = {}, P = {}> = (ctx: Context<C, P>, next: () => Promise<any>) => Promise<any>;

const BEFORES: Middleware[] = [
  // 拼接请求url
  async ctx => {
    ctx.body ||= ctx.b;

    const body = is("Object")(ctx.body) ? ctx.body : {};
    const keys = ctx.url
      .split("/")
      .filter(item => item.startsWith(":"))
      .map(item => item.substring(1));
    for (const key of keys) {
      const val = ctx.p[key] || body[key] || "";
      ctx.url = ctx.url.replace(new RegExp(`/:${key}\\??`, "g"), val ? `/${val}` : val);
    }

    ctx.url += "?";
    for (const [k, v] of Object.entries(ctx.q)) {
      Array.isArray(v)
        ? v.forEach(item => (ctx.url += `${k}=${item}&`))
        : ![null, undefined, ""].includes(v) && (ctx.url += `${k}=${v}&`);
    }
    ctx.url.substring(0, ctx.url.length - 1);
  },
];
const AFTERS: Middleware[] = [];
const ERRORS: Middleware[] = [];
const FINALS: Middleware[] = [];

export const createAPI = <T>(initial: Partial<Context<T>>) => {
  const context: Context = {
    q: {},
    b: {},
    p: {},
    body: {},
    baseURL: "",
    url: "",
    header: {},

    befores: [],
    afters: [],
    errors: [],
    finals: [],
  };

  mergeContext(context, initial);

  return {
    get: request<T>(context)("GET"),
    post: request<T>(context)("POST"),
    put: request<T>(context)("PUT"),
    del: request<T>(context)("DELETE"),
    connect: request<T>(context)("CONNECT"),
    trace: request<T>(context)("TRACE"),
    options: request<T>(context)("OPTIONS"),
    head: request<T>(context)("HEAD"),
    request: request<T>(context)(),

    use:
      <K extends "befores" | "afters" | "errors" | "finals">(key: K) =>
      (...args: Context[K]) =>
        context[key].push(...args),
  };
};

const request =
  <C>(context: Context) =>
  (method?: Context["method"]) =>
  <P extends object = {}, R = any>(conf1 = {} as Partial<Context<C, P>> | string) =>
  async <Result = R>(conf2 = {} as Partial<Context<C, P>>): Promise<Result> => {
    const ctx = merge({}, context, { deep: Infinity }) as Context;
    ctx.method = method;
    typeof conf1 == "string" ? (ctx.url = conf1) : mergeContext(ctx, conf1);
    mergeContext(ctx, conf2);
    await compose(
      ...ctx.befores,
      ...BEFORES
    )(ctx)
      .then(() => coreMiddleware(ctx))
      .then(() => compose(...AFTERS, ...ctx.afters)(ctx))
      .catch(error => compose(...ERRORS, ...ctx.errors)(merge(ctx, { error })))
      .finally(() => compose(...FINALS, ...ctx.finals)(ctx));
    return ctx.successResult?.data as Result;
  };

const mergeContext = (context1: Record<any, any>, context2: Record<any, any>) => {
  const keys = ["befores", "afters", "errors", "finals"];

  keys.forEach(key => {
    !Array.isArray(context1[key]) && (context1[key] = []);
    Array.isArray(context2[key]) && context1[key].push(...context2[key]);
  });

  return merge(context1, omit(context2, keys), {
    deep: Infinity,
  });
};

const coreMiddleware = async (ctx: Context) => {
  ctx.successResult = await new Promise((resolve, reject) => {
    ctx.method ||= "GET";
    uni.request({
      ...ctx,
      url: ctx.baseURL + ctx.url,
      data: ctx.body,
      success: data => resolve(data),
      fail: error => (ctx.failResult = error) && reject(error),
    });
  });
};
